
/* GCSx
** SCRIPT.CPP
**
** Script support
** Non-EDITOR members
*/

/*****************************************************************************
** Copyright (C) 2003-2006 Janson
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
** 
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111, USA.
*****************************************************************************/

#include "all.h"    
    
Script::Script(class World* myWorld, ScriptType type, int myId) : name(blankString), nameL(blankString), source() { start_func
    if (myId) assert(myWorld);
    
    id = myId;
    cached = 0;
    lockCount = 0;
    cacheFile = NULL;
    world = myWorld;
    bytecode = NULL;
    links = NULL;
    functions = NULL;
    diskStatus = CODE_UNCOMPILED;
    diskSource = 0;
    memStatus = CODE_UNCOMPILED;
    scriptType = type;
    defaultAnimgroup = NULL;
    defaultTileset = NULL;
    defaultId = 0;
    compileWarnings = compileErrors = 0;
}

Script::~Script() { start_func
    delete cacheFile;

    delete[] bytecode;
    delete links;
    destroyFunctionMap(functions);
    delete functions;
    
    // @TODO: For when you can copy stuff that includes refs to scripts/actors
    // clipboardClearScript(this);
}

void Script::cacheLoad() throw_File { start_func
    if (cached) {
        try {
            // load into memory
            assert(cacheFile);
            assert(cacheFile->getVersion() <= 1);
            assert(source.empty());

            source.clear();
            cacheFile->rewind();

            // @TODO: error checking on all of these, including unwinding
            // @TODO: skip loading source if not needed
            if (diskSource) {
                int rows = cacheFile->readInt();
                while (rows--) {
                    string data;
                    cacheFile->readStr(data);
                    source.push_back(data);
                }
            }
            
            if ((diskStatus >= CODE_COMPILED) && (memStatus < CODE_COMPILED)) {
                // Would've been recached if and only if diskStatus >= CODE_COMPILED
                assert(!bytecode);
                assert(!links);

                Uint32 size = cacheFile->readInt();
                bytecode = new Uint32[size + 2];
                bytecode[0] = size;
                cacheFile->readIntBulk(bytecode + 1, size + 1);
                
                links = new list<LinkEntry>;
                int count = cacheFile->readInt();
                while (count--) {
                    LinkEntry link;
                    link.type = (LinkEntry::LinkEntryType)(cacheFile->readInt());
                    link.offset = cacheFile->readInt();
                    cacheFile->readStr(link.scope);
                    cacheFile->readStr(link.name);
                    cacheFile->readStr(link.wart);
                    links->push_back(link);
                }
                
                memStatus = diskStatus;
            }

            // We retain this pointer and reuse it IF we
            // unlock to 0 AND aren't modified
            cached = 0;
        }
        catch (...) {
            // Recache
            source.clear();
            throw;
        }
    }
}

void Script::loadHeader(FileRead* file) throw_File { start_func
    assert(world);
    
    if (file->getVersion() > 1) {
        throw FileException("Unsupported script version %d", file->getVersion());
    }

    id = file->readInt();
    file->readStr(name);
    nameL = name;
    toLower(nameL);
    diskStatus = (ScriptStatus)file->readInt();
    diskSource = file->readInt();
    memStatus = CODE_UNCOMPILED;
    scriptType = (ScriptType)file->readInt();
    defaultAnimgroup = world->findAnimGroup(file->readInt());
    defaultTileset = world->findTileSet(file->readInt());
    defaultId = file->readInt();
    
    // Check validity
    if ((scriptType < SCRIPT_CODE) || (scriptType > SCRIPT_LIBRARY) ||
        (diskStatus < CODE_UNCOMPILED) || (diskStatus > CODE_COMPILED) ||
        (diskSource < 0) || (diskSource > 1) ||
        (defaultId < 0) || ((defaultAnimgroup) && (defaultTileset)) ||
        ((scriptType == SCRIPT_NOTE) && ((diskStatus > CODE_UNCOMPILED) || (defaultAnimgroup) || (defaultTileset))) ||
        ((scriptType == SCRIPT_LIBRARY) && ((defaultAnimgroup) || (defaultTileset))) ||
        ((defaultId) && (!defaultAnimgroup) && (!defaultTileset)) ||
        ((defaultAnimgroup) && (!defaultId)) ||
        ((defaultTileset) && (!defaultId))) {
        throw FileException("Corrupted script header");
    }

    // @TODO: Check validity
    if (diskStatus >= CODE_PARSED) {
        assert(!functions);
        assert(memStatus == CODE_UNCOMPILED);
        
        functions = new FunctionMap;
        int count = file->readInt();
        while (count--) {
            string name;
            file->readStr(name);
            
            Function& func = addFunctionMap(functions, name);
            func.numparam = file->readInt();
            if (func.numparam) {
                func.parameters = new DataType[func.numparam];
                for (int param = 0; param < func.numparam; ++param) {
                    func.parameters[param].baseType = file->readInt();
                    func.parameters[param].subType = file->readInt();
                    func.parameters[param].flags = file->readInt();
                }
            }
            else {
                func.parameters = NULL;
            }
            func.returntype.baseType = file->readInt();
            func.returntype.subType = file->readInt();
            func.returntype.flags = file->readInt();
            func.offset = file->readInt();
        }
        
        memStatus = CODE_PARSED;
    }
}

void Script::loadContent(FileRead* file) { start_func
    assert(world);
    
    delete cacheFile;
    cacheFile = file;
    cached = 1;
}

int Script::isContentCached() const { start_func
    return cached;
}

int Script::markLock() throw_File { start_func
    cacheLoad();
    return ++lockCount;
}

int Script::markUnlock() { start_func
    --lockCount;
    assert(lockCount >= 0);
    if ((lockCount == 0) && (cacheFile)) {
        // Recache ourselves
        cached = 1;
        source.clear();
        // Don't clear compile results if it's not on disk
        // Don't clear compile results if linked and we're retaining linked code
        if ((diskStatus >= CODE_COMPILED) &&
            ((memStatus != CODE_LINKED) || (!config->readNum(LINKED_RETAIN)))) {
            delete[] bytecode;
            delete links;
            bytecode = NULL;
            links = NULL;
            if (functions) memStatus = CODE_PARSED;
            else memStatus = CODE_UNCOMPILED;
        }
    }
    return lockCount;
}

int Script::isLocked() const { start_func
    return lockCount;
}

void Script::parseFuncs() { start_func
    assert(lockCount);
    
    if (memStatus == CODE_UNCOMPILED) {
        // Prepare to parse functions
        assert(!functions);
        functions = new FunctionMap;
        Compiler c(&source, world);
        debugWrite(DEBUG_COMPILE, "Pre-parsing '%s'...", name.c_str());

        // If error, destroy results
        if (c.parseSymbols(functions)) {
            destroyFunctionMap(functions);
            delete functions;
            functions = NULL;
        }
        else {
            // Otherwise, update status
            memStatus = CODE_PARSED;
        }

        compileWarnings = c.numWarnings();
        compileErrors = c.numErrors();
    }
}

void Script::compile() { start_func
    assert(lockCount);
    
    if (memStatus == CODE_UNCOMPILED) {
        parseFuncs();
        if (compileErrors) return;
    }
        
    if (memStatus == CODE_PARSED) {
        // Prepare to compile
        assert(!links);
        links = new list<LinkEntry>;
        Compiler c(&source, world);
    
        debugWrite("Compiling '%s'...", name.c_str());
        // If error, destroy results
        if (c.compile(&bytecode, functions, links, id)) {
            delete links;
            links = new list<LinkEntry>;
            delete[] bytecode;
            bytecode = NULL;
        }
        else {
            // Otherwise, update status
            memStatus = CODE_COMPILED;
        }
        
        compileWarnings = c.numWarnings();
        compileErrors = c.numErrors();
    }
}

void Script::doLink() { start_func
    assert(lockCount);
    assert(memStatus >= CODE_COMPILED);
    assert(bytecode);
    assert(!compileErrors);
    
    if (memStatus == CODE_COMPILED) {
        debugWrite(DEBUG_COMPILE, "Linking '%s'...", name.c_str());
        // @TODO: actual linking! if any errors can result, need
        // to prevent deletion of links in Script object, and also
        // cause situations in gameplay as well
        assert(links);
        memStatus = CODE_LINKED;
    }
}

void Script::collectGlobalLinks(World::GlobalMap* globalLinks, int* globalLinksCount) { start_func
    assert(lockCount);
    assert(memStatus >= CODE_COMPILED);
    assert(links);
    assert(!compileErrors);
    
    list<LinkEntry>::iterator pos = links->begin();
    list<LinkEntry>::iterator end = links->end();
    for (; pos != end; ++pos) {
        LinkEntry& link = *pos;
        
        if ((link.type == LinkEntry::LINK_GLOBAL) || (link.type == LinkEntry::LINK_GLOBALVAR)) {
            //link.wart
            World::GlobalMap::iterator prev = globalLinks->find(link.name.c_str());
            if (prev == globalLinks->end()) {
                // New global- just add it
                pair<int,const char*>& newGlobal = (*globalLinks)[newCpCopy(link.name)];
                newGlobal.first = (*globalLinksCount)++;
                newGlobal.second = newCpCopy(link.wart);
            }
            else {
                // Verify matches existing global
                if (link.wart != (*prev).second.second) {
                    // @TODO: Better output (debug window during gameplay; error window during editor)
                    debugWrite("ERROR in %s: Datatype of global variable '%s' differs from other scripts", getName().c_str(), link.name.c_str());
                    ++compileErrors;
                }
            }
        }
    }
}

void Script::link() { start_func
    doLink();
    
    // In non-edit mode, we can discard links now!
    delete links;
    links = NULL;
}

Uint32* Script::getCode() { start_func
    assert(bytecode);
    assert(memStatus == CODE_LINKED);
    return bytecode + 2;
}
